package sm.hibernate.persister;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.cache.CacheConcurrencyStrategy;
import org.hibernate.engine.Mapping;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.ValueInclusion;
import org.hibernate.engine.Versioning;
import org.hibernate.exception.JDBCExceptionHelper;
import org.hibernate.jdbc.Expectation;
import org.hibernate.jdbc.Expectations;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.SingleTableEntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.sql.JoinFragment;
import org.hibernate.type.ComponentType;
import org.hibernate.type.Type;
import org.hibernate.util.ArrayHelper;
import org.hibernate.util.StringHelper;

import sm.hibernate.sql.SMDelete;
import sm.hibernate.sql.SMUpdate;

public class SMEntityPersister extends SingleTableEntityPersister {

	private static final Log log = LogFactory
			.getLog(AbstractEntityPersister.class);

	private boolean[][] pColumnUpdateable;

	public boolean[] getPropertyColumnUpdateable(int i) {
		return pColumnUpdateable[i];
	}

	public boolean[] getPropertyColumnUpdateable(String[] columnNames) {
		boolean[] propertyColumnUpdateable = new boolean[columnNames.length];
		for (int i = 0; i < columnNames.length; i++) {
			propertyColumnUpdateable[i] = true;
		}
		return propertyColumnUpdateable;
	}

	public SMEntityPersister(PersistentClass persistentClass,
			CacheConcurrencyStrategy cache, SessionFactoryImplementor factory,
			Mapping mapping) throws HibernateException {
		super(persistentClass, cache, factory, mapping);
		int hydrateSpan = getEntityMetamodel().getPropertySpan();
		pColumnUpdateable = new boolean[hydrateSpan][];
		Iterator iter = persistentClass.getPropertyClosureIterator();
		int i = 0;
		while (iter.hasNext()) {
			Property prop = (Property) iter.next();
			pColumnUpdateable[i] = prop.getValue().getColumnUpdateability();
			i++;
		}
	}

	private boolean checkVersion(final boolean[] includeProperty) {
		return includeProperty[getVersionProperty()]
				|| getEntityMetamodel().getPropertyUpdateGenerationInclusions()[getVersionProperty()] != ValueInclusion.NONE;
	}

	/**
	 * Generate the SQL that updates a row by id (and version)
	 */
	protected String generateUpdateString(final boolean[] includeProperty,
			final int j, final Object[] oldFields, final boolean useRowId) {
		// customize: use SMUpdate
		SMUpdate update = new SMUpdate(getFactory().getDialect())
				.setTableName(getTableName(j));

		// select the correct row by either pk or rowid
		if (useRowId) {
			update.setPrimaryKeyColumnNames(new String[] { rowIdName }); // TODO:
																			// eventually,
																			// rowIdName[j]
		} else {
			update.setPrimaryKeyColumnNames(getKeyColumns(j));
		}

		boolean hasColumns = false;
		for (int i = 0; i < getEntityMetamodel().getPropertySpan(); i++) {
			if (includeProperty[i] && isPropertyOfTable(i, j)) {
				// this is a property of the table, which we are updating
				String[] propertyColumnNames = getPropertyColumnNames(i);
				update.addColumns(propertyColumnNames,
						getPropertyColumnUpdateable(propertyColumnNames));
				hasColumns = hasColumns || getPropertyColumnSpan(i) > 0;
			}
		}

		if (j == 0
				&& isVersioned()
				&& getEntityMetamodel().getOptimisticLockMode() == Versioning.OPTIMISTIC_LOCK_VERSION) {
			// this is the root (versioned) table, and we are using
			// version-based
			// optimistic locking; if we are not updating the version, also
			// don't
			// check it (unless this is a "generated" version column)!
			if (checkVersion(includeProperty)) {
				update.setVersionColumnName(getVersionColumnName());
				hasColumns = true;
			}
		} else if (getEntityMetamodel().getOptimisticLockMode() > Versioning.OPTIMISTIC_LOCK_VERSION
				&& oldFields != null) {
			// we are using "all" or "dirty" property-based optimistic locking

			boolean[] includeInWhere = getEntityMetamodel()
					.getOptimisticLockMode() == Versioning.OPTIMISTIC_LOCK_ALL ? getPropertyUpdateability()
					: // optimistic-lock="all", include all updatable properties
					includeProperty; // optimistic-lock="dirty", include all
										// properties we are updating this time

			boolean[] versionability = getPropertyVersionability();
			Type[] types = getPropertyTypes();
			for (int i = 0; i < getEntityMetamodel().getPropertySpan(); i++) {
				boolean include = includeInWhere[i] && isPropertyOfTable(i, j)
						&& versionability[i];
				if (include) {
					// this property belongs to the table, and it is not
					// specifically
					// excluded from optimistic locking by
					// optimistic-lock="false"
					String[] propertyColumnNames = getPropertyColumnNames(i);
					boolean[] propertyNullness = types[i].toColumnNullness(
							oldFields[i], getFactory());
					for (int k = 0; k < propertyNullness.length; k++) {
						if (propertyNullness[k]) {
							update.addWhereColumn(propertyColumnNames[k]);
						} else {
							update.addWhereColumn(propertyColumnNames[k],
									" is null");
						}
					}
				}
			}

		}

		if (getFactory().getSettings().isCommentsEnabled()) {
			update.setComment("update " + getEntityName());
		}

		return hasColumns ? update.toStatementString() : null;
	}

	@Override
	protected String generateDeleteString(int j) {
		// customize: use SMDelete
		SMDelete delete = new SMDelete().setTableName(getTableName(j))
				.setPrimaryKeyColumnNames(getKeyColumns(j));
		if (j == 0) {
			delete.setVersionColumnName(getVersionColumnName());
		}
		if (getFactory().getSettings().isCommentsEnabled()) {
			delete.setComment("delete " + getEntityName());
		}
		return delete.toStatementString();
	}

	// customize: handle null composite key, set ? twice
	private int writeNullableKey(Serializable id, SessionImplementor session,
			PreparedStatement st, int offset) throws SQLException {
		int index = offset;
		int identifierColumnSpan = getIdentifierColumnSpan();
		Type identifierType = getIdentifierType();
		if (identifierType instanceof ComponentType) {
			ComponentType componentIdType = (ComponentType) identifierType;
			Type[] propertyTypes = componentIdType.getSubtypes();
			Object[] subvalues = componentIdType.getPropertyValues(id,
					session.getEntityMode());
			for (int i = 0; i < identifierColumnSpan; i++) {
				int columnSpan = propertyTypes[i].getColumnSpan(session
						.getFactory());
				propertyTypes[i].nullSafeSet(st, subvalues[i], index, session);
				index += columnSpan;
				propertyTypes[i].nullSafeSet(st, subvalues[i], index, session);
				index += columnSpan;
			}
		} else {
			identifierType.nullSafeSet(st, id, index, session);
			index += identifierColumnSpan;
			identifierType.nullSafeSet(st, id, index, session);
			index += identifierColumnSpan;
		}
		return index;
	}

	@Override
	protected void delete(final Serializable id, final Object version,
			final int j, final Object object, final String sql,
			final SessionImplementor session, final Object[] loadedState)
			throws HibernateException {

		if (isInverseTable(j)) {
			return;
		}

		final boolean useVersion = j == 0 && isVersioned();
		final boolean callable = isDeleteCallable(j);
		final Expectation expectation = Expectations
				.appropriateExpectation(deleteResultCheckStyles[j]);
		final boolean useBatch = j == 0 && isBatchable()
				&& expectation.canBeBatched();

		if (log.isTraceEnabled()) {
			log.trace("Deleting entity: "
					+ MessageHelper.infoString(this, id, getFactory()));
			if (useVersion) {
				log.trace("Version: " + version);
			}
		}

		if (isTableCascadeDeleteEnabled(j)) {
			if (log.isTraceEnabled()) {
				log.trace("delete handled by foreign key constraint: "
						+ getTableName(j));
			}
			return; // EARLY EXIT!
		}

		try {
			// Render the SQL query
			PreparedStatement delete;
			int index = 1;
			if (useBatch) {
				if (callable) {
					delete = session.getBatcher()
							.prepareBatchCallableStatement(sql);
				} else {
					delete = session.getBatcher().prepareBatchStatement(sql);
				}
			} else {
				if (callable) {
					delete = session.getBatcher().prepareCallableStatement(sql);
				} else {
					delete = session.getBatcher().prepareStatement(sql);
				}
			}

			try {

				index += expectation.prepare(delete);

				// customize: handle null composite key, set ? twice
				index = writeNullableKey(id, session, delete, index);

				// We should use the _current_ object state (ie. after any
				// updates that occurred during flush)

				if (useVersion) {
					getVersionType().nullSafeSet(delete, version, index,
							session);
				} else if (getEntityMetamodel().getOptimisticLockMode() > Versioning.OPTIMISTIC_LOCK_VERSION
						&& loadedState != null) {
					boolean[] versionability = getPropertyVersionability();
					Type[] types = getPropertyTypes();
					for (int i = 0; i < getEntityMetamodel().getPropertySpan(); i++) {
						if (isPropertyOfTable(i, j) && versionability[i]) {
							// this property belongs to the table and it is not
							// specifically
							// excluded from optimistic locking by
							// optimistic-lock="false"
							boolean[] settable = types[i].toColumnNullness(
									loadedState[i], getFactory());
							types[i].nullSafeSet(delete, loadedState[i], index,
									settable, session);
							index += ArrayHelper.countTrue(settable);
						}
					}
				}

				if (useBatch) {
					session.getBatcher().addToBatch(expectation);
				} else {
					check(delete.executeUpdate(), id, j, expectation, delete);
				}

			} catch (SQLException sqle) {
				if (useBatch) {
					session.getBatcher().abortBatch(sqle);
				}
				throw sqle;
			} finally {
				if (!useBatch) {
					session.getBatcher().closeStatement(delete);
				}
			}

		} catch (SQLException sqle) {
			throw JDBCExceptionHelper.convert(getFactory()
					.getSQLExceptionConverter(), sqle, "could not delete: "
					+ MessageHelper.infoString(this, id, getFactory()), sql);
		}
	}

	@Override
	protected boolean update(final Serializable id, final Object[] fields,
			final Object[] oldFields, final Object rowId,
			final boolean[] includeProperty, final int j,
			final Object oldVersion, final Object object, final String sql,
			final SessionImplementor session) throws HibernateException {

		final boolean useVersion = j == 0 && isVersioned();
		final Expectation expectation = Expectations
				.appropriateExpectation(updateResultCheckStyles[j]);
		final boolean callable = isUpdateCallable(j);
		final boolean useBatch = j == 0 && expectation.canBeBatched()
				&& isBatchable(); // note: updates to joined tables can't be
									// batched...

		if (log.isTraceEnabled()) {
			log.trace("Updating entity: "
					+ MessageHelper.infoString(this, id, getFactory()));
			if (useVersion) {
				log.trace("Existing version: " + oldVersion
						+ " -> New version: " + fields[getVersionProperty()]);
			}
		}

		try {

			int index = 1; // starting index
			final PreparedStatement update;
			if (useBatch) {
				if (callable) {
					update = session.getBatcher()
							.prepareBatchCallableStatement(sql);
				} else {
					update = session.getBatcher().prepareBatchStatement(sql);
				}
			} else {
				if (callable) {
					update = session.getBatcher().prepareCallableStatement(sql);
				} else {
					update = session.getBatcher().prepareStatement(sql);
				}
			}

			try {
				index += expectation.prepare(update);

				if (log.isTraceEnabled()) {
					log.trace("Dehydrating entity: "
							+ MessageHelper.infoString(this, id, getFactory()));
				}

				for (int i = 0; i < getEntityMetamodel().getPropertySpan(); i++) {
					if (includeProperty[i] && isPropertyOfTable(i, j)) {
						getPropertyTypes()[i].nullSafeSet(update, fields[i],
								index, getPropertyColumnUpdateable(i), session);
						// index += getPropertyColumnSpan( i );
						index += ArrayHelper
								.countTrue(getPropertyColumnUpdateable(i)); // TODO:
																			// this
																			// is
																			// kinda
																			// slow...
					}
				}

				if (rowId != null) {
					update.setObject(index, rowId);
					index += 1;
				} else if (id != null) {
					index = writeNullableKey(id, session, update, index);
				}

				// Write any appropriate versioning conditional parameters
				if (useVersion
						&& Versioning.OPTIMISTIC_LOCK_VERSION == getEntityMetamodel()
								.getOptimisticLockMode()) {
					if (checkVersion(includeProperty)) {
						getVersionType().nullSafeSet(update, oldVersion, index,
								session);
					}
				} else if (getEntityMetamodel().getOptimisticLockMode() > Versioning.OPTIMISTIC_LOCK_VERSION
						&& oldFields != null) {
					boolean[] versionability = getPropertyVersionability(); // TODO:
																			// is
																			// this
																			// really
																			// necessary????
					boolean[] includeOldField = getEntityMetamodel()
							.getOptimisticLockMode() == Versioning.OPTIMISTIC_LOCK_ALL ? getPropertyUpdateability()
							: includeProperty;
					Type[] types = getPropertyTypes();
					for (int i = 0; i < getEntityMetamodel().getPropertySpan(); i++) {
						boolean include = includeOldField[i]
								&& isPropertyOfTable(i, j) && versionability[i]; // TODO:
																					// is
																					// this
																					// really
																					// necessary????
						if (include) {
							boolean[] settable = types[i].toColumnNullness(
									oldFields[i], getFactory());
							types[i].nullSafeSet(update, oldFields[i], index,
									settable, session);
							index += ArrayHelper.countTrue(settable);
						}
					}
				}

				if (useBatch) {
					session.getBatcher().addToBatch(expectation);
					return true;
				} else {
					return check(update.executeUpdate(), id, j, expectation,
							update);
				}

			} catch (SQLException sqle) {
				if (useBatch) {
					session.getBatcher().abortBatch(sqle);
				}
				throw sqle;
			} finally {
				if (!useBatch) {
					session.getBatcher().closeStatement(update);
				}
			}

		} catch (SQLException sqle) {
			throw JDBCExceptionHelper.convert(getFactory()
					.getSQLExceptionConverter(), sqle, "could not update: "
					+ MessageHelper.infoString(this, id, getFactory()), sql);
		}
	}

	@Override
	protected JoinFragment createJoin(String name, boolean innerJoin,
			boolean includeSubclasses) {
		final String[] idCols = StringHelper.qualify(name,
				getIdentifierColumnNames()); // all joins join to the pk of the
												// driving table
		final JoinFragment join = getFactory().getDialect()
				.createOuterJoinFragment();
		final int tableSpan = getSubclassTableSpan();
		for (int j = 1; j < tableSpan; j++) { // notice that we skip the first
												// table; it is the driving
												// table!
			final boolean joinIsIncluded = isClassOrSuperclassTable(j)
					|| (includeSubclasses
							&& !isSubclassTableSequentialSelect(j) && !isSubclassTableLazy(j));
			if (joinIsIncluded) {
				join.addJoin(
						getSubclassTableName(j),
						generateTableAlias(name, j),
						idCols,
						getSubclassTableKeyColumns(j),
						innerJoin && isClassOrSuperclassTable(j)
								&& !isInverseTable(j) && !isNullableTable(j) ? JoinFragment.INNER_JOIN
								: // we can inner join to superclass tables (the
									// row MUST be there)
								JoinFragment.LEFT_OUTER_JOIN // we can never
																// inner join to
																// subclass
																// tables
				);
			}
		}
		return join;
	}
}
